using System;
using System.Configuration;
using System.Data;
using TripleA.Core.Database;
using TripleA.Core.Database.SqlServer;
using TripleA.Core.Interfaces.Entities;
using TripleA.Core.Interfaces.Exceptions;

namespace TripleA.Core.Extensions.Database
{
    public class SqlServerCommandStep<T> : StepBase<T>
        where T : Context
    {
        public string ConnectionName { get; set; }
        public ISqlStatement Statement { get; set; }
        public int? ExpectedRows { get; set; }
        public Predicate<int> ExpectedRowsTest { get; set; }

        public SqlServerCommandStep()
        {
            ExpectedRowsTest = AssertActualAgainstExpected;
        }

        public override void PreValidate(T context)
        {
            if (Statement == null)
                throw new StepConfigurationException("Statement not set/provided");
        }

        public override void Execute(T context)
        {
            using (var cmd = BuildCommand())
            {
                var rows = cmd.ExecuteNonQuery();
                AssertRowCount(rows);
            }            
        }

        protected virtual string GetConnectionString()
        {
            if (!string.IsNullOrWhiteSpace(ConnectionName))
                return ConnectionName;
            
            var connectionName = ConfigurationManager.AppSettings["DefaultSqlConnection"];
            if (string.IsNullOrWhiteSpace(connectionName))
            {
                // not defined so use the only connection string (if one exists)
                if (ConfigurationManager.ConnectionStrings.Count == 1)
                    return ConfigurationManager.ConnectionStrings[0].ConnectionString;
                throw new ApplicationException("There are no connection strings defined!");
            }

            var cnn = ConfigurationManager.ConnectionStrings[connectionName];
            if (cnn == null)
                throw new ApplicationException(string.Format("AppSetting:DefaultSqlConnection '{0}' does not exist as a named connection string", connectionName));
            return cnn.ConnectionString;
        }

        protected virtual IDbCommand BuildCommand()
        {
            return SqlServerAdhocCommand.UsingSmartConnection(GetConnectionString())
                .WithSql(Statement)
                .Build();
        }

        protected void ExecuteReader(T context, Func<IDataReader, int, bool> rowHandler)
        {
            var i = 0;

            using (var cmd = BuildCommand())
            {
                using (var reader = cmd.ExecuteReader())
                {                    
                    while (reader.Read())
                    {
                        if (!rowHandler(reader, ++i))
                            break;
                    }
                }
            }

            AssertRowCount(i);
        }

        protected virtual int ExecuteGetInt(T context)
        {
            int result;

            using (var cmd = BuildCommand())
            {
                result = (int)cmd.ExecuteScalar();
            }

            AssertRowCount(result);
            return result;
        }

        protected void AssertRowCount(int actual)
        {
            if (!ExpectedRowsTest(actual))
                throw new ExpectationException(ExpectedExceptionMessage(ExpectedRows.GetValueOrDefault(actual), actual));
        }

        protected virtual string ExpectedExceptionMessage(int expected, int actual)
        {
            return string.Format("{0} rows affected, {1} expected", actual, expected);
        }

        private bool AssertActualAgainstExpected(int actual)
        {
            return ((ExpectedRows ?? actual) == actual);
        }
    }
}